package com.lambda.plugin;

import com.android.build.api.transform.Context;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public class FastClickTransform extends Transform {
    private final ASMConfig mConfig;
    private final String mVarName;

    FastClickTransform(ASMConfig config) {
        mConfig = config;
        mVarName = "_checker_" + System.currentTimeMillis();
    }

    @Override
    public String getName() {
        return FastClickTransform.class.getSimpleName();
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        _transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

    private void _transform(
            Context context,
            Collection<TransformInput> inputs,
            Collection<TransformInput> referencedInputs,
            TransformOutputProvider outputProvider,
            boolean isIncremental) throws IOException, TransformException, InterruptedException {
        if (!isIncremental) {
            outputProvider.deleteAll();
        }

        for (TransformInput transformInput : inputs) {
            for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) {
                File destDir = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY
                );
                FileUtils.copyDirectory(directoryInput.getFile(), destDir);
                traverse(destDir, directoryInput.getFile());
            }

            for (JarInput jarInput : transformInput.getJarInputs()) {
                String destName = jarInput.getName();
                if (destName.endsWith(".jar")) {
                    destName = destName.substring(0, destName.length() - 4);
                }

                String hexName = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath());
                File destDir = outputProvider.getContentLocation(
                        destName + "_" + hexName,
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR
                );

                FileUtils.copyFile(jarInput.getFile(), destDir);
            }
        }
    }

    private void traverse(File dest, File file) {
        List<File> buff = new ArrayList<>();
        buff.add(file);

        final String path = file.getAbsolutePath();

        while (!buff.isEmpty()) {
            File tmp = buff.remove(0);

            if (!tmp.isDirectory()) {
                String classPath = tmp.getAbsolutePath().substring(path.length() + 1);
                String className = classPath.replace(File.separatorChar, '.');
                if (packageTest(className)) {
                    File target = new File(dest + "/" + classPath);
                    try (
                            FileInputStream fis = new FileInputStream(tmp);
                            FileOutputStream fos = new FileOutputStream(target)
                    ) {
                        ClassReader cr = new ClassReader(fis);
                        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                        cr.accept(new MMClassVisitor(cw), ClassReader.EXPAND_FRAMES);
                        fos.write(cw.toByteArray());
                    } catch (Exception e) {
                        e.printStackTrace(System.out);
                    }
                }
            } else {
                File[] files = tmp.listFiles();
                if (files != null) {
                    buff.addAll(Arrays.asList(files));
                }
            }
        }
    }

    private boolean packageTest(String targetPakg) {
        for (String pakg : mConfig.pakgs) {
            if (targetPakg.startsWith(pakg)) {
                String tt = targetPakg.substring(pakg.length() + 1);
                return !tt.startsWith("R$") && !tt.startsWith("R.class") &&
                        !tt.startsWith("R2$") && !tt.startsWith("R2.class") &&
                        !tt.startsWith("BuildConfig");
            }
        }
        return false;
    }

    class MMClassVisitor extends ClassVisitor {
        private ClassVisitor mCvisit;
        private String owner;

        MMClassVisitor(ClassVisitor visitor) {
            super(Opcodes.ASM5, visitor);
            mCvisit = visitor;
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            if ("butterknife/internal/DebouncingOnClickListener".equals(superName)) {
                mCvisit.visitField(
                        Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, mVarName, mConfig.descriptor, signature, null
                );
                owner = name;
            } else {
                for (String iface : interfaces) {
                    if ("android/view/View$OnClickListener".equals(iface)) {
                        mCvisit.visitField(
                                Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, mVarName, mConfig.descriptor, signature, null
                        );
                        owner = name;
                        break;
                    }
                }
            }
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor visitor = super.visitMethod(access, name, desc, signature, exceptions);
            if (owner != null) {
                switch (name) {
                    case "<init>":
                        return new InitVisitor(visitor, owner);

                    case "onClick":
                    case "doClick":
                        return new OnClickVisitor(visitor, owner);
                }
            }
            return visitor;
        }
    }

    class InitVisitor extends MethodVisitor {
        private final String mOwner;

        public InitVisitor(MethodVisitor methodVisitor, String owner) {
            super(Opcodes.ASM5, methodVisitor);
            mOwner = owner;
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                mv.visitTypeInsn(Opcodes.NEW, mConfig.className);
                mv.visitInsn(Opcodes.DUP);
                mv.visitMethodInsn(Opcodes.INVOKESPECIAL, mConfig.className, "<init>", "()V", false);
                mv.visitFieldInsn(Opcodes.PUTFIELD, mOwner, mVarName, mConfig.descriptor);
            }
            super.visitInsn(opcode);
        }
    }

    class OnClickVisitor extends MethodVisitor {
        private final String mOwner;

        public OnClickVisitor(MethodVisitor methodVisitor, String owner) {
            super(Opcodes.ASM5, methodVisitor);
            mOwner = owner;
        }

        @Override
        public void visitCode() {
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, mOwner, mVarName, mConfig.descriptor);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, mConfig.className, mConfig.method, "()Z", false);
            Label label = new Label();
            mv.visitJumpInsn(Opcodes.IFNE, label);
            mv.visitInsn(Opcodes.RETURN);
            mv.visitLabel(label);
            super.visitCode();
        }
    }
}
